package registry

import (
	"bytes"
	"encoding/json"
	"log"
	"net/http"
	"sync"
	"time"
)

const ServerPort = "3000"
const ServicesURL = "http://localhost:" + ServerPort + "/services"

type registry struct {
	registrations      map[ServiceName]Registration
	registrationsMutex *sync.RWMutex
}

func (r *registry) add(reg Registration) error {
	r.registrationsMutex.Lock()
	r.registrations[reg.ServiceName] = reg
	r.registrationsMutex.Unlock()

	err := r.sendRequiredServices(reg)
	if err != nil {
		return err
	}

	r.notify(&patch{
		Added: []patchEntry{
			{
				reg.ServiceName,
				reg.ServiceURL,
			},
		},
	})

	return nil
}

func (r *registry) notify(fullPatch *patch) {
	r.registrationsMutex.RLock()
	defer r.registrationsMutex.RUnlock()

	for _, reg := range r.registrations {
		go func(reg Registration) {
			for _, reqService := range reg.RequiredServices {
				p := patch{Added: []patchEntry{}, Removed: []patchEntry{}}
				sendUpdate := false
				for _, added := range fullPatch.Added {
					if added.Name == reqService {
						p.Added = append(p.Added, added)
						sendUpdate = true
					}
				}
				for _, removed := range fullPatch.Removed {
					if removed.Name == reqService {
						p.Removed = append(p.Removed, removed)
						sendUpdate = true
					}
				}
				if sendUpdate {
					err := r.sendPatch(p, reg.ServiceUpdateURL)
					if err != nil {
						log.Println(err)
						return
					}
				}
			}
		}(reg)
	}
}

func (r *registry) sendRequiredServices(reg Registration) error {
	r.registrationsMutex.RLock()
	defer r.registrationsMutex.RUnlock()

	var p patch
	for _, reqServiceName := range reg.RequiredServices {
		reqService, ok := r.registrations[reqServiceName]
		if ok {
			p.Added = append(p.Added, patchEntry{
				Name: reqService.ServiceName,
				URL:  reqService.ServiceURL,
			})
		}
	}

	err := r.sendPatch(p, reg.ServiceUpdateURL)
	if err != nil {
		return err
	}
	return nil
}

func (r *registry) sendPatch(p patch, url string) error {
	d, err := json.Marshal(p)
	if err != nil {
		return err
	}
	_, err = http.Post(url, "application/json", bytes.NewBuffer(d))
	if err != nil {
		return err
	}
	return nil
}

func (r *registry) remove(serviceName ServiceName) error {
	r.registrationsMutex.Lock()
	removedService, ok := r.registrations[serviceName]
	if ok {
		delete(r.registrations, serviceName)
	}
	r.registrationsMutex.Unlock()

	if ok {
		r.notify(&patch{Removed: []patchEntry{
			{
				Name: removedService.ServiceName,
				URL:  removedService.ServiceURL,
			},
		}})
	}
	return nil
}

func (r *registry) heartbeat(freq time.Duration) {
	for {
		var wg sync.WaitGroup
		for _, reg := range r.registrations {
			wg.Add(1)
			go func(reg Registration) {
				defer wg.Done()
				success := true
				for attempts := 0; attempts < 3; attempts++ {
					res, err := http.Get(reg.HeartbeatURL)
					if err != nil {
						log.Println(err)
					} else if res.StatusCode == http.StatusOK {
						log.Printf("Heartbeat check passed for %v", reg.ServiceName)
						if !success {
							r.add(reg)
						}
						break
					}
					log.Printf("Heartbeat check failed for %v", reg.ServiceName)

					if success {
						success = false
						r.remove(reg.ServiceName)
					}

					time.Sleep(1 * time.Second)
				}
			}(reg)
			wg.Wait()
			time.Sleep(freq)
		}
	}
}

var reg = registry{
	registrations:      make(map[ServiceName]Registration),
	registrationsMutex: &sync.RWMutex{},
}

var once sync.Once

func SetupRegistryService() {
	once.Do(func() {
		go reg.heartbeat(3 * time.Second)
	})
}
